La estructura básica

Toda aplicación que creemos en shiny consta de al menos:

  • Una interfaz gráfica, UI.

  • Una función de servidor, SERVER.

  • La introducción de una función para correr la app, shinyApp().

library(shiny)

# Define UI for application
ui <- fluidPage(

)

# Define server logic required
server <- function(input, output) {


}

# Run the application 
shinyApp(ui = ui, server = server)

Dentro de cualquier shiny app se puede dar clic al botón “Run app”

Personalización de la interfaz gráfica (ui)

Colocación de elementos dentro de la página

Existen muchos tipos de formatos que se pueden utilizar y todas estas formas de estructurar las páginas se pueden combinar unas con otras en un sinfín de posibilidades.

Diseño de barra lateral, sidebarLayout

La función sidebarLayout(sidebarPanel, mainPanel, position = c("left", "right")) nos permite crear una app que contenga una barra secundaria por un lado (izq. o der.), y un panel principal. Para esto, especificamos dentro de esta función las funciones sidebarPanel() y mainPanel(), respectivamente.

Cada panel puede contener uno o más objetos.

ui <- fluidPage(
  # Panel de título
  titlePanel("Título de la app"),
  
  # Especificación de la separación entre panel lateral y principal
  sidebarLayout(
    
    # Panel lateral
    sidebarPanel("Título del panel"),
    
    # Panel principal
    mainPanel("Título del main panel")
    )
)

Acomodando los objetos en una barra lateral y un panel principal con sidebarLayout

Filas de objetos, fluidRow

Si se desean acomodar objetos por filas, se puede utilizar el formato fluidRow(), donde se especifican columnas con column(), y lo que cada una contendrá.

El ancho de cada columna se especifica con un número del 1 al 12. Preferentemente, la suma de todas las columnas debería ser 12.

ui <- fluidPage(
  # Panel de título
  titlePanel("Título de la app"),
  
  # Primera fila
  fluidRow(
    column(width = 12,
           "objeto de la columna única")
  ),
  # Segunda fila
  fluidRow(
    column(width = 2,
           "objetos de la col. 1"),
    column(width = 5,
           "objetos de la col. 2"),
    column(width = 5,
           "objetos de la col. 3")
  )
)

Dos filas hechas con fluidRow

Diseño de objetos fluidos, flowLayout

Si se quieren acomodar varios objetos de forma consecutiva (de izquierda a derecha) y que éstos vayan tomando distintas filas, dependiendo del tamaño de la ventana, se puede utilizar flowLayout.

ui <- fluidPage(
  # Panel de título
  titlePanel("Título de la app"),
  
  flowLayout(
    "objeto 1",
    
    "objeto 2",
    
    "objeto 3",
    
    "etc."
  )
)

flowLayout con una ventana amplia

flowLayout con una ventana angosta

Objetos horizontalmente, splitLayout

Esta estructura asigna el mismo tamaño a todos los objetos (al menos de manera predeterminada), y los acomoda de manera horizontal. Aquí no son fluidos. Su tamaño cambiará para ajustarse al tamaño de la ventana, pero siempre mantendrán su acomodo.

ui <- fluidPage(
  # Panel de título
  titlePanel("Título de la app"),
  
  splitLayout(
    "objeto 1",
    
    "objeto 2",
    
    "objeto 3",
    
    "etc."
  )
)

Objetos verticalmente, verticalLayout

Si se desean acomodar objetos acomodados de manera vertical entre sí, se puede utilizar esta estructura.

ui <- fluidPage(
  # Panel de título
  titlePanel("Título de la app"),
  
  verticalLayout(
    "objeto 1",
    
    "objeto 2",
    
    "objeto 3",
    
    "etc."
  )
)

verticalLayout en conjunto con flowLayout


Agregar capas de objetos con tabPanels

Independientemente del tipo de presentación de las capas que se seleccione, cada capa debe ir especificada con tabPanel().

Panel con pestañas, tabsetPanel

Estos tabPanels pueden ser de dos tipos: pestañas tabs (defáult) y píldoras pills.

tabs

ui <- fluidPage(
  # Panel de título
  titlePanel("Título de la app"),
  
  tabsetPanel(type = "tabs",
    
    tabPanel("Panel 1",
             ...
             ),
    
    tabPanel("Panel 2",
             ...
             ),
    
    tabPanel("Panel 3",
             ...
             )
  )
)

pills

El código es casi igual, solo cambia el argumento type = "pills".

Página con barra de navegación, navbarPage

Este formato crea una barra de navegación con botones para ls distintos tabPanels:

ui <- fluidPage(
  # Panel de título
  titlePanel("Título de la app"),
  
  navbarPage("Título de la barra",
    
    tabPanel("Panel 1",
             ...
             ),
    
    tabPanel("Panel 2",
             ...
             ),
    
    tabPanel("Panel 3",
             ...
             )
  )
)

Panel con lista de navegación, navlistPanel

Se puede crear una lista de navegación, que incluya varios paneles para mostrar distinta información.

ui <- fluidPage(
  # Panel de título
  titlePanel("Título de la app"),
  
  navlistPanel(
    
    tabPanel("Panel 1",
             ...
             ),
    
    tabPanel("Panel 2",
             ...
             ),
    
    tabPanel("Panel 3",
             ...
             )
  )
)

Funciones HTML

Las shiny apps, al igual que los R Notebooks o R Markdown funcionan a base de HTML, por lo que se pueden utilizar funciones sin mayor problema.

Algunas de estas son:

  • h#() para distintos niveles de encabezados.

  • p() para generar párrafos.

  • strong() para poner texto en negritas.

  • em() genera texto en cursiva.

  • code() pone texto en formato de código.

ui <- fluidPage(
    titlePanel("Título de la app"),
    sidebarLayout(
        sidebarPanel("sidebar panel"),
        mainPanel("main panel",
                  h1("encabezado nivel 1"),
                  h2("encabezado nivel 2"),
                  h3("encabezado nivel 3"),
                  p("Inicia párrafo con texto normal,", 
                    strong("texto en negritas dentro del párrafo,"),
                    em("también texto en cursiva"), "y",
                    code("texto con formato de código")),
                  p("Un nuevo párrafo."))
    )
)

Temas

La paquetería shinythemes cuenta con varios temas preinstalados que se pueden agregar fácilmente a la app. Para utilizarlos, basta con mandar llamar la librería y, dentro de la interfaz de usuario definir theme = shinytheme("tema escogido"), para que su app se muestre así. Pueden ver una lista de los temas soportados actuales aquí. Por ejemplo:

shinyUI(navbarPage(title = "Data analysis app",
                   
                   theme = shinytheme("united"),
                   
                   tabPanel("Seattle House prices",
                   )
)
)

Más personalización con HTML y CSS

Si están familiarizados con HTML y CSS, se puede personalizar cuanto quieran su aplicación o partes de ella.

P. ej., se puede agregar un archivo con todas las características CSS. Para esto, se necesita poner en una carpeta (dentro del directorio de la app) que lleve por nombre “www” y para cargar el tema, se haría de manera similar que con los shinythemes. Asumiendo que su archivo CSS se llama “bootstrap.css”, se podría cargar así:

shinyUI(navbarPage(title = "Data analysis app",
                   
                   theme = "bootstrap.css",
                   
                   tabPanel("Seattle House prices",
                   )
)
)

Carga de imágenes

Se pueden incluir imágenes dentro de la app, como logotipos, fondos, etc. Los archivos deben estar guardados también en la carpeta “wwww” para que R pueda encontrarlos.

La imagen se puede definir dentro de cualquier tipo de interfaz seleccionada con la función img().

img(src = "image.jpg", 
                height = "70%", width = "70%", align = "center")

Objetos en shiny

Hasta ahora solo hemos visto cómo se puede personalizar la app. Sin embargo, no hemos agregado ningún objeto con el que el usuario pueda interactuar (inputs) o visualizar después de su interacción (outputs).

Entradas del usuario, inputs

Existen muchos tipos de objeto, creados para que el usuario pueda interactuar con la app. Todos estos inputs se definen dentro de la interfaz de usuario, ui.

Entre ellos:

  • Botones de acción, actionButton()

  • Casillas de verificación:

    • múltiples, checkboxGroupInput()
    • sencillas, checkboxInput()
  • Selectores de fecha:

    • una fecha en particular, dateInput()
    • un rango de fechas, dateRangeInput()
  • Carga de archivos, fileInput()

  • Valores numéricos, numericInput()

  • Botones de radio, radioButtons()

  • Listas desplegables, selectInput()

  • Barra deslizante, sliderInput()

  • Botón de enviar, submitButton()

  • Entradas de texto, textInput()

  • Selección de colores, colourInput() * disponible con la paquetería colourpicker.

Cada uno de estos objetos de input debe tener su propio identificador único, inputId, que, de hecho, es el primer argumento de cada función. Con este ID mandaremos llamar las entradas del usuario para modificar los resultados en la parte del servidor (server).

Respuesta de la app, outputs

Para poder mostrar gráficas, tablas, texto, etc. una vez que el usuario interactuó con los inputs, es necesario definir (todavía dentro de la ui) el tipo de output que queremos mostrar.

Estas funciones de output van acompañadas de una función similar en la parte del servidor. Debajo se muestran varias funciones comunes, junto con su par para la parte del servidor.

Función de la ui Descripción Función del server
plotOutput() Gráfica estática renderPlot()
plotlyOutput() Gráfica interactiva renderPlotly()
tableOutput() Tabla de datos renderTable()
dataTableOutput() Tabla interactiva renderDataTable()
textOutput() Texto con formato renderText()
verbatimTextOutput() Texto de consola renderPrint()
imageOutput() Imagen renderImage()

Configuración del servidor, server

LS0tDQp0aXRsZTogIkxvcyBlbGVtZW50b3MgYsOhc2ljb3MgZGUgdW5hIFNoaW55IEFwcCINCmF1dGhvcjogIlBhYmxvIEJlbmF2aWRlcy1IZXJyZXJhIg0KZGF0ZTogMjAyMC0wNi0xMw0Kb3V0cHV0OiANCiAgaHRtbF9ub3RlYm9vazoNCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6IFRSVUUNCiAgICB0aGVtZTogdW5pdGVkDQogICAgaGlnaGxpZ2h0OiB0YW5nbw0KLS0tDQoNCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGV2YWwgPSBGQUxTRSkNCmBgYA0KDQoNCiMgTGEgZXN0cnVjdHVyYSBiw6FzaWNhDQoNClRvZGEgYXBsaWNhY2nDs24gcXVlIGNyZWVtb3MgZW4gc2hpbnkgY29uc3RhIGRlIGFsIG1lbm9zOg0KDQoqIFVuYSBpbnRlcmZheiBncsOhZmljYSwgKipVSSoqLg0KDQoqIFVuYSBmdW5jacOzbiBkZSBzZXJ2aWRvciwgKipTRVJWRVIqKi4NCg0KKiBMYSBpbnRyb2R1Y2Npw7NuIGRlIHVuYSBmdW5jacOzbiBwYXJhIGNvcnJlciBsYSBhcHAsIGBzaGlueUFwcCgpYC4NCg0KDQpgYGB7ciBzaGlueS1zbmlwcGV0LCBldmFsID0gRkFMU0V9DQpsaWJyYXJ5KHNoaW55KQ0KDQojIERlZmluZSBVSSBmb3IgYXBwbGljYXRpb24NCnVpIDwtIGZsdWlkUGFnZSgNCg0KKQ0KDQojIERlZmluZSBzZXJ2ZXIgbG9naWMgcmVxdWlyZWQNCnNlcnZlciA8LSBmdW5jdGlvbihpbnB1dCwgb3V0cHV0KSB7DQoNCg0KfQ0KDQojIFJ1biB0aGUgYXBwbGljYXRpb24gDQpzaGlueUFwcCh1aSA9IHVpLCBzZXJ2ZXIgPSBzZXJ2ZXIpDQoNCmBgYA0KDQpEZW50cm8gZGUgY3VhbHF1aWVyICpzaGlueSBhcHAqIHNlIHB1ZWRlIGRhciBjbGljIGFsIGJvdMOzbiAqKiJSdW4gYXBwIioqDQoNCg0KIyBQZXJzb25hbGl6YWNpw7NuIGRlIGxhIGludGVyZmF6IGdyw6FmaWNhIChgdWlgKQ0KDQojIyBDb2xvY2FjacOzbiBkZSBlbGVtZW50b3MgZGVudHJvIGRlIGxhIHDDoWdpbmEgey50YWJzZXR9DQoNCkV4aXN0ZW4gbXVjaG9zIHRpcG9zIGRlIGZvcm1hdG9zIHF1ZSBzZSBwdWVkZW4gdXRpbGl6YXIgeSB0b2RhcyBlc3RhcyBmb3JtYXMgZGUgZXN0cnVjdHVyYXIgbGFzIHDDoWdpbmFzIHNlIHB1ZWRlbiBjb21iaW5hciB1bmFzIGNvbiBvdHJhcyBlbiB1biBzaW5mw61uIGRlIHBvc2liaWxpZGFkZXMuDQoNCiMjIyBEaXNlw7FvIGRlIGJhcnJhIGxhdGVyYWwsIGBzaWRlYmFyTGF5b3V0YA0KDQpMYSBmdW5jacOzbiBgc2lkZWJhckxheW91dChzaWRlYmFyUGFuZWwsIG1haW5QYW5lbCwgcG9zaXRpb24gPSBjKCJsZWZ0IiwgInJpZ2h0IikpYCBub3MgcGVybWl0ZSBjcmVhciB1bmEgYXBwIHF1ZSBjb250ZW5nYSB1bmEgYmFycmEgc2VjdW5kYXJpYSBwb3IgdW4gbGFkbyAoKippenEuKiogbyBkZXIuKSwgeSB1biBwYW5lbCBwcmluY2lwYWwuIFBhcmEgZXN0bywgZXNwZWNpZmljYW1vcyBkZW50cm8gZGUgZXN0YSBmdW5jacOzbiBsYXMgZnVuY2lvbmVzIGBzaWRlYmFyUGFuZWwoKWAgeSBgbWFpblBhbmVsKClgLCByZXNwZWN0aXZhbWVudGUuDQoNCkNhZGEgcGFuZWwgcHVlZGUgY29udGVuZXIgdW5vIG8gbcOhcyBvYmpldG9zLg0KDQpgYGB7ciBzaWRlYmFyTGF5b3V0fQ0KdWkgPC0gZmx1aWRQYWdlKA0KICAjIFBhbmVsIGRlIHTDrXR1bG8NCiAgdGl0bGVQYW5lbCgiVMOtdHVsbyBkZSBsYSBhcHAiKSwNCiAgDQogICMgRXNwZWNpZmljYWNpw7NuIGRlIGxhIHNlcGFyYWNpw7NuIGVudHJlIHBhbmVsIGxhdGVyYWwgeSBwcmluY2lwYWwNCiAgc2lkZWJhckxheW91dCgNCiAgICANCiAgICAjIFBhbmVsIGxhdGVyYWwNCiAgICBzaWRlYmFyUGFuZWwoIlTDrXR1bG8gZGVsIHBhbmVsIiksDQogICAgDQogICAgIyBQYW5lbCBwcmluY2lwYWwNCiAgICBtYWluUGFuZWwoIlTDrXR1bG8gZGVsIG1haW4gcGFuZWwiKQ0KICAgICkNCikNCmBgYA0KDQohW0Fjb21vZGFuZG8gbG9zIG9iamV0b3MgZW4gdW5hIGJhcnJhIGxhdGVyYWwgeSB1biBwYW5lbCBwcmluY2lwYWwgY29uIGBzaWRlYmFyTGF5b3V0YF0oZmlncy9zaWRlYmFyTGF5b3V0LnBuZykNCg0KIyMjIEZpbGFzIGRlIG9iamV0b3MsIGBmbHVpZFJvd2ANCg0KU2kgc2UgZGVzZWFuIGFjb21vZGFyIG9iamV0b3MgcG9yIGZpbGFzLCBzZSBwdWVkZSB1dGlsaXphciBlbCBmb3JtYXRvIGBmbHVpZFJvdygpYCwgZG9uZGUgc2UgZXNwZWNpZmljYW4gKipjb2x1bW5hcyoqIGNvbiBgY29sdW1uKClgLCB5IGxvIHF1ZSBjYWRhIHVuYSBjb250ZW5kcsOhLg0KDQpFbCBhbmNobyBkZSBjYWRhIGNvbHVtbmEgc2UgZXNwZWNpZmljYSBjb24gdW4gbsO6bWVybyBkZWwgKioxIGFsIDEyKiouIFByZWZlcmVudGVtZW50ZSwgbGEgc3VtYSBkZSB0b2RhcyBsYXMgY29sdW1uYXMgZGViZXLDrWEgc2VyICoqMTIqKi4NCg0KYGBge3IgZmx1aWRSb3d9DQp1aSA8LSBmbHVpZFBhZ2UoDQogICMgUGFuZWwgZGUgdMOtdHVsbw0KICB0aXRsZVBhbmVsKCJUw610dWxvIGRlIGxhIGFwcCIpLA0KICANCiAgIyBQcmltZXJhIGZpbGENCiAgZmx1aWRSb3coDQogICAgY29sdW1uKHdpZHRoID0gMTIsDQogICAgICAgICAgICJvYmpldG8gZGUgbGEgY29sdW1uYSDDum5pY2EiKQ0KICApLA0KICAjIFNlZ3VuZGEgZmlsYQ0KICBmbHVpZFJvdygNCiAgICBjb2x1bW4od2lkdGggPSAyLA0KICAgICAgICAgICAib2JqZXRvcyBkZSBsYSBjb2wuIDEiKSwNCiAgICBjb2x1bW4od2lkdGggPSA1LA0KICAgICAgICAgICAib2JqZXRvcyBkZSBsYSBjb2wuIDIiKSwNCiAgICBjb2x1bW4od2lkdGggPSA1LA0KICAgICAgICAgICAib2JqZXRvcyBkZSBsYSBjb2wuIDMiKQ0KICApDQopDQpgYGANCg0KIVtEb3MgZmlsYXMgaGVjaGFzIGNvbiBgZmx1aWRSb3dgXShmaWdzL2ZsdWlkUm93LlBORykNCg0KIyMjIERpc2XDsW8gZGUgb2JqZXRvcyBmbHVpZG9zLCBgZmxvd0xheW91dGANCg0KU2kgc2UgcXVpZXJlbiBhY29tb2RhciB2YXJpb3Mgb2JqZXRvcyBkZSBmb3JtYSBjb25zZWN1dGl2YSAoZGUgaXpxdWllcmRhIGEgZGVyZWNoYSkgeSBxdWUgw6lzdG9zIHZheWFuIHRvbWFuZG8gZGlzdGludGFzIGZpbGFzLCBkZXBlbmRpZW5kbyBkZWwgdGFtYcOxbyBkZSBsYSB2ZW50YW5hLCBzZSBwdWVkZSB1dGlsaXphciBgZmxvd0xheW91dGAuDQoNCmBgYHtyIGZsb3dMYXlvdXR9DQp1aSA8LSBmbHVpZFBhZ2UoDQogICMgUGFuZWwgZGUgdMOtdHVsbw0KICB0aXRsZVBhbmVsKCJUw610dWxvIGRlIGxhIGFwcCIpLA0KICANCiAgZmxvd0xheW91dCgNCiAgICAib2JqZXRvIDEiLA0KICAgIA0KICAgICJvYmpldG8gMiIsDQogICAgDQogICAgIm9iamV0byAzIiwNCiAgICANCiAgICAiZXRjLiINCiAgKQ0KKQ0KYGBgDQoNCiFbYGZsb3dMYXlvdXRgIGNvbiB1bmEgdmVudGFuYSBhbXBsaWFdKGZpZ3MvZmxvd0xheW91dF93aWRlLlBORykNCg0KIVtgZmxvd0xheW91dGAgY29uIHVuYSB2ZW50YW5hIGFuZ29zdGFdKGZpZ3MvZmxvd0xheW91dF9sb25nLlBORykNCg0KIyMjIE9iamV0b3MgaG9yaXpvbnRhbG1lbnRlLCBgc3BsaXRMYXlvdXRgDQoNCkVzdGEgZXN0cnVjdHVyYSBhc2lnbmEgZWwgbWlzbW8gdGFtYcOxbyBhIHRvZG9zIGxvcyBvYmpldG9zIChhbCBtZW5vcyBkZSBtYW5lcmEgcHJlZGV0ZXJtaW5hZGEpLCB5IGxvcyBhY29tb2RhIGRlIG1hbmVyYSBob3Jpem9udGFsLiBBcXXDrSBubyBzb24gZmx1aWRvcy4gU3UgdGFtYcOxbyBjYW1iaWFyw6EgcGFyYSBhanVzdGFyc2UgYWwgdGFtYcOxbyBkZSBsYSB2ZW50YW5hLCBwZXJvIHNpZW1wcmUgbWFudGVuZHLDoW4gc3UgYWNvbW9kby4NCg0KYGBge3Igc3BsaXRMYXlvdXR9DQp1aSA8LSBmbHVpZFBhZ2UoDQogICMgUGFuZWwgZGUgdMOtdHVsbw0KICB0aXRsZVBhbmVsKCJUw610dWxvIGRlIGxhIGFwcCIpLA0KICANCiAgc3BsaXRMYXlvdXQoDQogICAgIm9iamV0byAxIiwNCiAgICANCiAgICAib2JqZXRvIDIiLA0KICAgIA0KICAgICJvYmpldG8gMyIsDQogICAgDQogICAgImV0Yy4iDQogICkNCikNCmBgYA0KDQohW10oZmlncy9zcGxpdExheW91dC5QTkcpDQoNCiMjIyBPYmpldG9zIHZlcnRpY2FsbWVudGUsIGB2ZXJ0aWNhbExheW91dGANClNpIHNlIGRlc2VhbiBhY29tb2RhciBvYmpldG9zIGFjb21vZGFkb3MgZGUgbWFuZXJhIHZlcnRpY2FsIGVudHJlIHPDrSwgc2UgcHVlZGUgdXRpbGl6YXIgZXN0YSBlc3RydWN0dXJhLg0KDQpgYGB7ciB2ZXJpY2FsTGF5b3V0fQ0KdWkgPC0gZmx1aWRQYWdlKA0KICAjIFBhbmVsIGRlIHTDrXR1bG8NCiAgdGl0bGVQYW5lbCgiVMOtdHVsbyBkZSBsYSBhcHAiKSwNCiAgDQogIHZlcnRpY2FsTGF5b3V0KA0KICAgICJvYmpldG8gMSIsDQogICAgDQogICAgIm9iamV0byAyIiwNCiAgICANCiAgICAib2JqZXRvIDMiLA0KICAgIA0KICAgICJldGMuIg0KICApDQopDQpgYGANCg0KIVtgdmVydGljYWxMYXlvdXRgIGVuIGNvbmp1bnRvIGNvbiBgZmxvd0xheW91dGBdKGZpZ3MvdmVydGljYWxMYXlvdXQuUE5HKQ0KDQoNCg0KKioqDQoNCiMjIEFncmVnYXIgY2FwYXMgZGUgb2JqZXRvcyBjb24gYHRhYlBhbmVsc2Agey50YWJzZXQgLnRhYnNldC1waWxsc30NCg0KSW5kZXBlbmRpZW50ZW1lbnRlIGRlbCB0aXBvIGRlIHByZXNlbnRhY2nDs24gZGUgbGFzIGNhcGFzIHF1ZSBzZSBzZWxlY2Npb25lLCBjYWRhIGNhcGEgZGViZSBpciBlc3BlY2lmaWNhZGEgY29uIGB0YWJQYW5lbCgpYC4gDQoNCiMjIyBQYW5lbCBjb24gcGVzdGHDsWFzLCBgdGFic2V0UGFuZWxgDQoNCkVzdG9zIGB0YWJQYW5lbHNgIHB1ZWRlbiBzZXIgZGUgZG9zIHRpcG9zOiAqKnBlc3Rhw7FhcyBgdGFic2AqKiAoZGVmw6F1bHQpIHkgcMOtbGRvcmFzIGBwaWxsc2AuDQoNCiMjIyMgYHRhYnNgDQoNCmBgYHtyIHRhYnNldFBhbmVsLXRhYnN9DQp1aSA8LSBmbHVpZFBhZ2UoDQogICMgUGFuZWwgZGUgdMOtdHVsbw0KICB0aXRsZVBhbmVsKCJUw610dWxvIGRlIGxhIGFwcCIpLA0KICANCiAgdGFic2V0UGFuZWwodHlwZSA9ICJ0YWJzIiwNCiAgICANCiAgICB0YWJQYW5lbCgiUGFuZWwgMSIsDQogICAgICAgICAgICAgLi4uDQogICAgICAgICAgICAgKSwNCiAgICANCiAgICB0YWJQYW5lbCgiUGFuZWwgMiIsDQogICAgICAgICAgICAgLi4uDQogICAgICAgICAgICAgKSwNCiAgICANCiAgICB0YWJQYW5lbCgiUGFuZWwgMyIsDQogICAgICAgICAgICAgLi4uDQogICAgICAgICAgICAgKQ0KICApDQopDQpgYGANCg0KIVtdKGZpZ3MvdGFic2V0UGFuZWwtdGFicy5QTkcpDQoNCiMjIyMgYHBpbGxzYA0KDQpFbCBjw7NkaWdvIGVzIGNhc2kgaWd1YWwsIHNvbG8gY2FtYmlhIGVsIGFyZ3VtZW50byBgdHlwZSA9ICJwaWxscyJgLg0KDQoNCiFbXShmaWdzL3RhYnNldFBhbmVsLXBpbGxzLlBORykNCg0KDQojIyMgUMOhZ2luYSBjb24gYmFycmEgZGUgbmF2ZWdhY2nDs24sIGBuYXZiYXJQYWdlYA0KDQpFc3RlIGZvcm1hdG8gY3JlYSB1bmEgYmFycmEgZGUgbmF2ZWdhY2nDs24gY29uIGJvdG9uZXMgcGFyYSBscyBkaXN0aW50b3MgYHRhYlBhbmVsc2A6DQoNCmBgYHtyIG5hdmJhclBhZ2V9DQp1aSA8LSBmbHVpZFBhZ2UoDQogICMgUGFuZWwgZGUgdMOtdHVsbw0KICB0aXRsZVBhbmVsKCJUw610dWxvIGRlIGxhIGFwcCIpLA0KICANCiAgbmF2YmFyUGFnZSgiVMOtdHVsbyBkZSBsYSBiYXJyYSIsDQogICAgDQogICAgdGFiUGFuZWwoIlBhbmVsIDEiLA0KICAgICAgICAgICAgIC4uLg0KICAgICAgICAgICAgICksDQogICAgDQogICAgdGFiUGFuZWwoIlBhbmVsIDIiLA0KICAgICAgICAgICAgIC4uLg0KICAgICAgICAgICAgICksDQogICAgDQogICAgdGFiUGFuZWwoIlBhbmVsIDMiLA0KICAgICAgICAgICAgIC4uLg0KICAgICAgICAgICAgICkNCiAgKQ0KKQ0KYGBgDQoNCiFbXShmaWdzL25hdmJhclBhZ2UuUE5HKQ0KDQoNCiMjIyBQYW5lbCBjb24gbGlzdGEgZGUgbmF2ZWdhY2nDs24sIGBuYXZsaXN0UGFuZWxgDQoNClNlIHB1ZWRlIGNyZWFyIHVuYSBsaXN0YSBkZSBuYXZlZ2FjacOzbiwgcXVlIGluY2x1eWEgdmFyaW9zIHBhbmVsZXMgcGFyYSBtb3N0cmFyIGRpc3RpbnRhIGluZm9ybWFjacOzbi4NCg0KYGBge3IgbmF2bGlzdFBhbmVsfQ0KdWkgPC0gZmx1aWRQYWdlKA0KICAjIFBhbmVsIGRlIHTDrXR1bG8NCiAgdGl0bGVQYW5lbCgiVMOtdHVsbyBkZSBsYSBhcHAiKSwNCiAgDQogIG5hdmxpc3RQYW5lbCgNCiAgICANCiAgICB0YWJQYW5lbCgiUGFuZWwgMSIsDQogICAgICAgICAgICAgLi4uDQogICAgICAgICAgICAgKSwNCiAgICANCiAgICB0YWJQYW5lbCgiUGFuZWwgMiIsDQogICAgICAgICAgICAgLi4uDQogICAgICAgICAgICAgKSwNCiAgICANCiAgICB0YWJQYW5lbCgiUGFuZWwgMyIsDQogICAgICAgICAgICAgLi4uDQogICAgICAgICAgICAgKQ0KICApDQopDQpgYGANCg0KIVtdKGZpZ3MvbmF2bGlzdFBhbmVsLlBORykNCg0KIyMjIHstfQ0KDQojIyBGdW5jaW9uZXMgYEhUTUxgDQoNCkxhcyBzaGlueSBhcHBzLCBhbCBpZ3VhbCBxdWUgbG9zIFIgTm90ZWJvb2tzIG8gUiBNYXJrZG93biBmdW5jaW9uYW4gYSBiYXNlIGRlIEhUTUwsIHBvciBsbyBxdWUgc2UgcHVlZGVuIHV0aWxpemFyIGZ1bmNpb25lcyBzaW4gbWF5b3IgcHJvYmxlbWEuDQoNCkFsZ3VuYXMgZGUgZXN0YXMgc29uOg0KDQoqIGBoIygpYCBwYXJhIGRpc3RpbnRvcyBuaXZlbGVzIGRlIGVuY2FiZXphZG9zLg0KDQoqIGBwKClgIHBhcmEgZ2VuZXJhciBww6FycmFmb3MuDQoNCiogYHN0cm9uZygpYCBwYXJhIHBvbmVyIHRleHRvIGVuICoqbmVncml0YXMqKi4NCg0KKiBgZW0oKWAgZ2VuZXJhIHRleHRvIGVuICpjdXJzaXZhKi4NCg0KKiBgY29kZSgpYCBwb25lIHRleHRvIGVuIGZvcm1hdG8gZGUgYGPDs2RpZ29gLg0KDQpgYGB7ciwgZXZhbD1GQUxTRX0NCnVpIDwtIGZsdWlkUGFnZSgNCiAgICB0aXRsZVBhbmVsKCJUw610dWxvIGRlIGxhIGFwcCIpLA0KICAgIHNpZGViYXJMYXlvdXQoDQogICAgICAgIHNpZGViYXJQYW5lbCgic2lkZWJhciBwYW5lbCIpLA0KICAgICAgICBtYWluUGFuZWwoIm1haW4gcGFuZWwiLA0KICAgICAgICAgICAgICAgICAgaDEoImVuY2FiZXphZG8gbml2ZWwgMSIpLA0KICAgICAgICAgICAgICAgICAgaDIoImVuY2FiZXphZG8gbml2ZWwgMiIpLA0KICAgICAgICAgICAgICAgICAgaDMoImVuY2FiZXphZG8gbml2ZWwgMyIpLA0KICAgICAgICAgICAgICAgICAgcCgiSW5pY2lhIHDDoXJyYWZvIGNvbiB0ZXh0byBub3JtYWwsIiwgDQogICAgICAgICAgICAgICAgICAgIHN0cm9uZygidGV4dG8gZW4gbmVncml0YXMgZGVudHJvIGRlbCBww6FycmFmbywiKSwNCiAgICAgICAgICAgICAgICAgICAgZW0oInRhbWJpw6luIHRleHRvIGVuIGN1cnNpdmEiKSwgInkiLA0KICAgICAgICAgICAgICAgICAgICBjb2RlKCJ0ZXh0byBjb24gZm9ybWF0byBkZSBjw7NkaWdvIikpLA0KICAgICAgICAgICAgICAgICAgcCgiVW4gbnVldm8gcMOhcnJhZm8uIikpDQogICAgKQ0KKQ0KYGBgDQoNCiMjIFRlbWFzDQoNCkxhIHBhcXVldGVyw61hIGBzaGlueXRoZW1lc2AgY3VlbnRhIGNvbiB2YXJpb3MgdGVtYXMgcHJlaW5zdGFsYWRvcyBxdWUgc2UgcHVlZGVuIGFncmVnYXIgZsOhY2lsbWVudGUgYSBsYSBhcHAuIFBhcmEgdXRpbGl6YXJsb3MsIGJhc3RhIGNvbiBtYW5kYXIgbGxhbWFyIGxhIGxpYnJlcsOtYSB5LCBkZW50cm8gZGUgbGEgaW50ZXJmYXogZGUgdXN1YXJpbyBkZWZpbmlyIGB0aGVtZSA9IHNoaW55dGhlbWUoInRlbWEgZXNjb2dpZG8iKWAsIHBhcmEgcXVlIHN1IGFwcCBzZSBtdWVzdHJlIGFzw60uIFB1ZWRlbiB2ZXIgdW5hIGxpc3RhIGRlIGxvcyB0ZW1hcyBzb3BvcnRhZG9zIGFjdHVhbGVzIFthcXXDrV0oaHR0cHM6Ly9ib290c3dhdGNoLmNvbS8zLykuIFBvciBlamVtcGxvOg0KDQoNCmBgYHtyIHNoaW55dGhlbWVzfQ0Kc2hpbnlVSShuYXZiYXJQYWdlKHRpdGxlID0gIkRhdGEgYW5hbHlzaXMgYXBwIiwNCiAgICAgICAgICAgICAgICAgICANCiAgICAgICAgICAgICAgICAgICB0aGVtZSA9IHNoaW55dGhlbWUoInVuaXRlZCIpLA0KICAgICAgICAgICAgICAgICAgIA0KICAgICAgICAgICAgICAgICAgIHRhYlBhbmVsKCJTZWF0dGxlIEhvdXNlIHByaWNlcyIsDQogICAgICAgICAgICAgICAgICAgKQ0KKQ0KKQ0KYGBgDQoNCiMjIE3DoXMgcGVyc29uYWxpemFjacOzbiBjb24gSFRNTCB5IENTUw0KDQpTaSBlc3TDoW4gZmFtaWxpYXJpemFkb3MgY29uIEhUTUwgeSBDU1MsIHNlIHB1ZWRlIHBlcnNvbmFsaXphciBjdWFudG8gcXVpZXJhbiBzdSBhcGxpY2FjacOzbiBvIHBhcnRlcyBkZSBlbGxhLg0KDQpQLiBlai4sIHNlIHB1ZWRlIGFncmVnYXIgdW4gYXJjaGl2byBjb24gdG9kYXMgbGFzIGNhcmFjdGVyw61zdGljYXMgQ1NTLiBQYXJhIGVzdG8sIHNlIG5lY2VzaXRhIHBvbmVyIGVuIHVuYSBjYXJwZXRhICgqZGVudHJvIGRlbCBkaXJlY3RvcmlvIGRlIGxhIGFwcCopIHF1ZSBsbGV2ZSBwb3Igbm9tYnJlICIqKnd3dyoqIiB5IHBhcmEgY2FyZ2FyIGVsIHRlbWEsIHNlIGhhcsOtYSBkZSBtYW5lcmEgc2ltaWxhciBxdWUgY29uIGxvcyBgc2hpbnl0aGVtZXNgLiBBc3VtaWVuZG8gcXVlIHN1IGFyY2hpdm8gQ1NTIHNlIGxsYW1hICJib290c3RyYXAuY3NzIiwgc2UgcG9kcsOtYSBjYXJnYXIgYXPDrToNCg0KYGBge3IgY3NzfQ0Kc2hpbnlVSShuYXZiYXJQYWdlKHRpdGxlID0gIkRhdGEgYW5hbHlzaXMgYXBwIiwNCiAgICAgICAgICAgICAgICAgICANCiAgICAgICAgICAgICAgICAgICB0aGVtZSA9ICJib290c3RyYXAuY3NzIiwNCiAgICAgICAgICAgICAgICAgICANCiAgICAgICAgICAgICAgICAgICB0YWJQYW5lbCgiU2VhdHRsZSBIb3VzZSBwcmljZXMiLA0KICAgICAgICAgICAgICAgICAgICkNCikNCikNCmBgYA0KDQojIyBDYXJnYSBkZSBpbcOhZ2VuZXMNCg0KU2UgcHVlZGVuIGluY2x1aXIgaW3DoWdlbmVzIGRlbnRybyBkZSBsYSBhcHAsIGNvbW8gbG9nb3RpcG9zLCBmb25kb3MsIGV0Yy4gTG9zIGFyY2hpdm9zIGRlYmVuIGVzdGFyIGd1YXJkYWRvcyB0YW1iacOpbiBlbiBsYSBjYXJwZXRhICIqKnd3d3cqKiIgcGFyYSBxdWUgKipSKiogcHVlZGEgZW5jb250cmFybG9zLg0KDQpMYSBpbWFnZW4gc2UgcHVlZGUgZGVmaW5pciBkZW50cm8gZGUgY3VhbHF1aWVyIHRpcG8gZGUgaW50ZXJmYXogc2VsZWNjaW9uYWRhIGNvbiBsYSBmdW5jacOzbiBgaW1nKClgLg0KDQpgYGB7ciBpbWd9DQppbWcoc3JjID0gImltYWdlLmpwZyIsIA0KICAgICAgICAgICAgICAgIGhlaWdodCA9ICI3MCUiLCB3aWR0aCA9ICI3MCUiLCBhbGlnbiA9ICJjZW50ZXIiKQ0KYGBgDQoNCg0KIyBPYmpldG9zIGVuIHNoaW55DQoNCkhhc3RhIGFob3JhIHNvbG8gaGVtb3MgdmlzdG8gY8OzbW8gc2UgcHVlZGUgcGVyc29uYWxpemFyIGxhIGFwcC4gU2luIGVtYmFyZ28sIG5vIGhlbW9zIGFncmVnYWRvIG5pbmfDum4gb2JqZXRvIGNvbiBlbCBxdWUgZWwgdXN1YXJpbyBwdWVkYSBpbnRlcmFjdHVhciAoYGlucHV0c2ApIG8gdmlzdWFsaXphciBkZXNwdcOpcyBkZSBzdSBpbnRlcmFjY2nDs24gKGBvdXRwdXRzYCkuDQoNCg0KIyMgRW50cmFkYXMgZGVsIHVzdWFyaW8sIGBpbnB1dHNgDQoNCkV4aXN0ZW4gbXVjaG9zIHRpcG9zIGRlIG9iamV0bywgY3JlYWRvcyBwYXJhIHF1ZSBlbCB1c3VhcmlvIHB1ZWRhIGludGVyYWN0dWFyIGNvbiBsYSBhcHAuIFRvZG9zIGVzdG9zIGBpbnB1dHNgIHNlIGRlZmluZW4gZGVudHJvIGRlIGxhIGludGVyZmF6IGRlIHVzdWFyaW8sIGB1aWAuDQoNCkVudHJlIGVsbG9zOg0KDQoqIEJvdG9uZXMgZGUgYWNjacOzbiwgYGFjdGlvbkJ1dHRvbigpYA0KDQoqIENhc2lsbGFzIGRlIHZlcmlmaWNhY2nDs246DQogIC0gbcO6bHRpcGxlcywgYGNoZWNrYm94R3JvdXBJbnB1dCgpYA0KICAtIHNlbmNpbGxhcywgYGNoZWNrYm94SW5wdXQoKWANCg0KKiBTZWxlY3RvcmVzIGRlIGZlY2hhOg0KICAtIHVuYSBmZWNoYSBlbiBwYXJ0aWN1bGFyLCBgZGF0ZUlucHV0KClgDQogIC0gdW4gcmFuZ28gZGUgZmVjaGFzLCBgZGF0ZVJhbmdlSW5wdXQoKWANCiAgDQoqIENhcmdhIGRlIGFyY2hpdm9zLCBgZmlsZUlucHV0KClgDQoNCiogVmFsb3JlcyBudW3DqXJpY29zLCBgbnVtZXJpY0lucHV0KClgDQoNCiogQm90b25lcyBkZSByYWRpbywgYHJhZGlvQnV0dG9ucygpYA0KDQoqIExpc3RhcyBkZXNwbGVnYWJsZXMsIGBzZWxlY3RJbnB1dCgpYA0KDQoqIEJhcnJhIGRlc2xpemFudGUsIGBzbGlkZXJJbnB1dCgpYA0KDQoqIEJvdMOzbiBkZSBlbnZpYXIsIGBzdWJtaXRCdXR0b24oKWANCg0KKiBFbnRyYWRhcyBkZSB0ZXh0bywgYHRleHRJbnB1dCgpYA0KDQoqIFNlbGVjY2nDs24gZGUgY29sb3JlcywgYGNvbG91cklucHV0KClgICogKmRpc3BvbmlibGUgY29uIGxhIHBhcXVldGVyw61hKiBgY29sb3VycGlja2VyYC4NCg0KQ2FkYSB1bm8gZGUgZXN0b3Mgb2JqZXRvcyBkZSBpbnB1dCBkZWJlIHRlbmVyIHN1IHByb3BpbyBpZGVudGlmaWNhZG9yICoqw7puaWNvKiosIGBpbnB1dElkYCwgcXVlLCBkZSBoZWNobywgZXMgZWwgcHJpbWVyIGFyZ3VtZW50byBkZSBjYWRhIGZ1bmNpw7NuLiBDb24gZXN0ZSBJRCBtYW5kYXJlbW9zIGxsYW1hciBsYXMgZW50cmFkYXMgZGVsIHVzdWFyaW8gcGFyYSBtb2RpZmljYXIgbG9zIHJlc3VsdGFkb3MgZW4gbGEgcGFydGUgZGVsIHNlcnZpZG9yIChgc2VydmVyYCkuDQoNCiMjIFJlc3B1ZXN0YSBkZSBsYSBhcHAsIGBvdXRwdXRzYA0KDQpQYXJhIHBvZGVyIG1vc3RyYXIgZ3LDoWZpY2FzLCB0YWJsYXMsIHRleHRvLCBldGMuIHVuYSB2ZXogcXVlIGVsIHVzdWFyaW8gaW50ZXJhY3R1w7MgY29uIGxvcyBgaW5wdXRzYCwgZXMgbmVjZXNhcmlvIGRlZmluaXIgKHRvZGF2w61hIGRlbnRybyBkZSBsYSBgdWlgKSBlbCAgdGlwbyBkZSBgb3V0cHV0YCBxdWUgcXVlcmVtb3MgbW9zdHJhci4NCg0KRXN0YXMgZnVuY2lvbmVzIGRlIGBvdXRwdXRgIHZhbiBhY29tcGHDsWFkYXMgZGUgdW5hIGZ1bmNpw7NuIHNpbWlsYXIgZW4gbGEgcGFydGUgZGVsIHNlcnZpZG9yLiBEZWJham8gc2UgbXVlc3RyYW4gdmFyaWFzIGZ1bmNpb25lcyBjb211bmVzLCBqdW50byBjb24gc3UgcGFyIHBhcmEgbGEgcGFydGUgZGVsIHNlcnZpZG9yLg0KDQoNCnwgRnVuY2nDs24gZGUgbGEgYHVpYCB8IERlc2NyaXBjacOzbiB8IEZ1bmNpw7NuIGRlbCBgc2VydmVyYCAgfA0KfDotLS0tLS0tLS0tLS0tLS0tLS06fDotLS0tLS0tLS0tLTp8Oi0tLS0tLS0tLS0tLS0tLS0tLS0tLTp8DQp8IGBwbG90T3V0cHV0KClgfCBHcsOhZmljYSBlc3TDoXRpY2EgfCBgcmVuZGVyUGxvdCgpYCAgICAgICAgfA0KfCBgcGxvdGx5T3V0cHV0KClgfEdyw6FmaWNhIGludGVyYWN0aXZhIHwgYHJlbmRlclBsb3RseSgpYCAgfA0KfCBgdGFibGVPdXRwdXQoKWAgfCBUYWJsYSBkZSBkYXRvcyB8IGByZW5kZXJUYWJsZSgpYCAgICAgICB8DQp8IGBkYXRhVGFibGVPdXRwdXQoKWB8VGFibGEgaW50ZXJhY3RpdmF8YHJlbmRlckRhdGFUYWJsZSgpYHwNCnwgYHRleHRPdXRwdXQoKWAgfCBUZXh0byBjb24gZm9ybWF0byB8IGByZW5kZXJUZXh0KClgICAgICAgfA0KfCBgdmVyYmF0aW1UZXh0T3V0cHV0KClgfFRleHRvIGRlIGNvbnNvbGEgfCBgcmVuZGVyUHJpbnQoKWB8DQp8IGBpbWFnZU91dHB1dCgpYCAgICB8IEltYWdlbiAgICAgIHwgYHJlbmRlckltYWdlKClgICAgICAgIHwNCg0KDQojIENvbmZpZ3VyYWNpw7NuIGRlbCBzZXJ2aWRvciwgYHNlcnZlcmANCg0KDQoNCg0KDQoNCg0K